Skip to content

feat: ts/packages/cloudflare-hub, a Worker deployment of a hub node - #14

Merged
Mearman merged 6 commits into
mainfrom
feat/cloudflare-hub
Sep 10, 2026
Merged

feat: ts/packages/cloudflare-hub, a Worker deployment of a hub node#14
Mearman merged 6 commits into
mainfrom
feat/cloudflare-hub

Conversation

@Mearman

@Mearman Mearman commented Sep 10, 2026

Copy link
Copy Markdown
Member

Plan task #6: a reference Cloudflare Worker deployment of a public, always-on hub node, depending on ts/packages/core as an ordinary workspace consumer.

The ports architecture is what makes a Worker possible at all — no net.Server, no full node:crypto — so rather than reinventing protocol logic, this package supplies Worker-shaped adapters for core's existing port contracts:

  • Transport: WebSocket messages instead of TCP streams (each binary message is one self-delimiting CBOR frame, no length prefix). Inbound connections arrive via the fetch handler's WebSocket upgrade through acceptPair(); listen() registers the handler. Undecodable bytes reject that connection; a decodable but schema-invalid frame drops without disconnecting — mirroring core's TCP adapter's split.
  • Identity: Web Crypto ECDSA P-256, device-id = SHA-256 of the raw public-key bytes (never certificate DER). Signature interop with core's Node identity adapter is proven in both directions by tests.

The hub logic itself is deliberately thin: relay-connect pairing via gossiped device-ids, bidirectional relay-data forwarding, forget-on-disconnect, and newest-gossip-wins for a device's mapping. 15 tests cover the pairing logic, the transport adapter's hostile-input behaviour, and identity interop.

CI: the ts job (renamed ts-verify, turbo runs workspace-wide) now also runs _build, which for this package is wrangler deploy --dry-run --outdir=dist — validating the Worker bundle with no Cloudflare credentials. Deferred and documented in the package README: raw-TCP ingress via cloudflare:sockets, the announcer role (needs a KV/DO Storage adapter over core's Storage port), and a real authenticated deploy.

@Mearman
Mearman marked this pull request as ready for review September 10, 2026 16:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-10T16:42:27.174874Z 514ad73 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman force-pushed the feat/cloudflare-hub branch from f999ca5 to 8812b41 Compare September 10, 2026 17:37
A reference Cloudflare Worker running a public, always-on hub, depending on core as an ordinary workspace consumer. The ports architecture is what makes a Worker possible at all -- no net.Server, no full node:crypto -- so this package supplies Worker-shaped adapters for the same contracts: a WebSocket-message Transport (each binary message is one self-delimiting CBOR frame; upgrades arrive through the fetch handler via acceptPair rather than a bound listener) and a Web Crypto Identity deriving device-id from the raw public-key bytes, with signature interop proven against core's Node adapter in both directions. The hub logic itself is deliberately thin: relay-connect pairing via gossiped device-ids, bidirectional relay-data forwarding, and cleanup on disconnect.

Hostile input mirrors core's TCP adapter: undecodable bytes reject that connection; a decodable but schema-invalid frame drops without disconnecting. Deferred and documented in the package README: raw-TCP ingress via cloudflare:sockets, the announcer role (needs a KV/Durable-Object Storage adapter), and a real deploy (wrangler deploy --dry-run validates the bundle in CI without credentials).
turbo runs workspace-wide, so the renamed ts-verify job already linted/typechecked/tested the hub; adding the _build step also exercises its wrangler deploy --dry-run bundle validation. README's Implementations section records the hub alongside core.
… relayed a frame

The pull-based for-await over receive() parks on a pure-JS waiter, and workerd's hang detection cancels any plain-Worker request whose promise chain does that -- so the old entry logged "the Workers runtime canceled this request because it detected that your Worker's code had hung" once per connection and relayed zero frames, while looking alive (the socket-level listeners still fired, and wrangler deploy --dry-run passed because bundling executes nothing). A Durable Object is the documented home for exactly this lifetime shape: the DO class is defined in the entrypoint (wrangler resolves the binding against the entrypoint's own exports) and owns the hub state, accepting each upgrade's server socket and driving the hub per connection; the default export becomes a thin router forwarding upgrades to the single named instance. The SQLite-backed class per current wrangler guidance makes hibernation available as a follow-up without a migration later.

The transport adapter loses its now-dead listen/acceptPair factory (the DO drives wrapWebSocket directly) and keeps the Connection contract as its whole surface. scripts/live-check.mjs drives two genuine WebSocket clients through gossip -> relay-connect -> relay-inbound -> bidirectional relay-data against wrangler dev -- the check dry-run bundling cannot make -- verified passing three consecutive runs with zero hang-cancellation errors in the dev log. Plain-JS scripts join the eslint ignores (no TS project to type them against); .wrangler/ joins .gitignore.
…rings both ways

A rejecting receive iteration (undecodable bytes or a non-binary message -- the adapter's own hostile-input behaviour) used to reject handleConnection, which the entry voids: an unhandled rejection on every hostile client. The rejection is the disconnect signal and the adapter already closed the socket, so handleConnection now treats it exactly like a clean end: registry and pairing cleanup run identically.

A second relay-connect from the same initiator used to overwrite only the initiator-side pairing entry, leaving the old target's side live: the old target's relay-data kept flowing to the initiator on a pipe the initiator believed was its new pairing -- mis-attributed traffic in the exact role the package exists to serve. A new relay-connect now tears any pairing either side already belongs to down in both directions before installing the new one.

Tests grow accordingly, with FakeWebSocket extracted to a shared helper: the rejection-cleanup path, the stale-pairing case (B's data must not reach A after A re-pairs to C), text-message and send-after-close handling on the adapter, a byte-identical re-encode assertion, and -- the seam where the entry bug hid -- hub tests driving the real wrapWebSocket adapter rather than fakes alone, including hostile bytes closing only the offending connection.
The teardown only covered the same-role entries: a connection already the TARGET of one pairing (X->A) that then initiated its own (A->B) kept X's pipe half-alive -- X's relay-data kept flowing to A on what A believed was its pipe with B -- and symmetrically for a target holding its own outbound pipe. The teardown is now a single forgetPairingsOf helper removing every pairing a connection belongs to in either role, both directions of each, applied to both the initiator and the target before installing a new pairing; forgetConnection reuses the same helper. Cross-role tests pin both directions: an old opposite-end's data arrives nowhere after either re-pair shape.
…check claim

The adapter and wrangler.toml headers still referenced src/durable-hub.ts, folded into src/worker.ts when the Durable Object class moved into the entrypoint. The README's verification prose now states what the live check proves structurally (exits non-zero naming the failing step on regression) rather than a point-in-time run count. Also pins the identity adapter's unsupported-alg behaviour with a test: an unimplemented COSE algorithm throws loudly rather than mis-verifying.
@Mearman
Mearman force-pushed the feat/cloudflare-hub branch from 8812b41 to 3171ee4 Compare September 10, 2026 18:02
@Mearman
Mearman merged commit 7cef550 into main Sep 10, 2026
5 checks passed
@Mearman
Mearman deleted the feat/cloudflare-hub branch September 10, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant